﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Configuration;
using System.IO;
using System.Reflection;
using NBuild.Fx.Interfaces;

namespace NBuild.Fx.Engine
{
    public sealed class NBuildEngine : IBuildEngine
    {
        #region Input fields

        //All sources to be compiled
        private readonly StringCollection _sources = new StringCollection();

        //Assemblies to be referenced for compilation. Can be either full path to the DLL file or
        //the name of the DLL. The extension should be specified.
        private readonly StringCollection _references = new StringCollection();

        //.NET language in which the script is written
        private string _language;

        private readonly StringCollection _startupTargets = new StringCollection();
        private readonly PropertyOverrideCollection _propertyOverrides = new PropertyOverrideCollection();

        #endregion

        #region Processing fields

        private CompilerResults _compilerResults;
        private TargetCollection _targets;
        private string[] _initialTargets;

        private Type _buildType;
        private NBuildScript _buildInstance;
        private readonly Dictionary<string, PropertyInfo> _propertyMappings = new Dictionary<string, PropertyInfo>();

        #endregion

        #region Runtime fields

        private readonly BuildEnvironment _buildEnvironment = new BuildEnvironment();
        private readonly Dictionary<Type, object> _taskPool = new Dictionary<Type, object>();

        #endregion

        public NBuildEngine()
        {
            AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve;
        }

        private Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            var assemblyName = new AssemblyName(args.Name);
            foreach (var reference in _references)
            {
                string fullPath = Path.GetFullPath(reference);
                string filename = Path.GetFileNameWithoutExtension(fullPath);
                if (filename.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase))
                    return Assembly.LoadFile(fullPath);
            }
            return null;
        }

        public NBuildResults Execute(ExecutionMode executionMode)
        {
            try
            {
                LoadConfiguration();
                if (executionMode == ExecutionMode.Debug)
                    LoadDebugAssembly();
                else
                    CompileScriptAssembly();
                if (_compilerResults.Errors.HasErrors)
                    return new NBuildResults(_compilerResults);
                Process();
                if (executionMode == ExecutionMode.Run || executionMode == ExecutionMode.Debug)
                    Run();
                return new NBuildResults();
            }
            catch (ProcessingException pe)
            {
                return new NBuildResults(pe);
            }
            catch (TargetInvocationException tie)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(tie.InnerException);
                throw tie.InnerException;
            }
        }

        #region Input properties

        public string Language
        {
            get { return _language; }
            set { _language = value; }
        }

        public PropertyOverrideCollection PropertyOverrides
        {
            get { return _propertyOverrides; }
        }

        public StringCollection References
        {
            get { return _references; }
        }

        public StringCollection Sources
        {
            get { return _sources; }
        }

        public StringCollection StartupTargets
        {
            get
            {
                return _startupTargets;
            }
        }

        public bool TreatWarningsAsErrors { get; set; }

        #endregion

        #region Processing methods

        private void LoadConfiguration()
        {
            var configuration = (NBuildSection)ConfigurationManager.GetSection(NBuildSection.SectionPath);
            if (configuration == null)
            {
                _language = "cs";
                return;
            }

            if (string.IsNullOrEmpty(_language))
            {
                if (!CodeDomProvider.IsDefinedLanguage(configuration.Language))
                    throw new ProcessingException("Default language specified in configuration {0} is invalid", configuration.Language);
                _language = configuration.Language;
            }

            if (configuration.References != null)
            {
                NBuildReferenceElement[] references = configuration.References.ToArray();
                foreach (var reference in references)
                    _references.Add(reference.File);
            }

            if (configuration.ReferencePaths != null)
            {
                NBuildReferencePathElement[] referencePaths = configuration.ReferencePaths.ToArray();
                foreach (var referencePath in referencePaths)
                    _references.AddRange(Directory.GetFiles(referencePath.Path, "*.dll"));
            }
        }

        private void LoadDebugAssembly()
        {
            _compilerResults = new CompilerResults(new TempFileCollection());
            _compilerResults.CompiledAssembly = Assembly.GetEntryAssembly();
        }

        private void CompileScriptAssembly()
        {
            if (!CodeDomProvider.IsDefinedLanguage(_language))
                throw new ProcessingException("The language you have specified {0} is invalid", _language);

            CodeDomProvider provider = CodeDomProvider.CreateProvider(Language);

            var options = new CompilerParameters
            {
                GenerateExecutable = false,
                GenerateInMemory = true,
                TreatWarningsAsErrors = TreatWarningsAsErrors,
                ReferencedAssemblies =
                {
                    "mscorlib.dll",
                    "System.dll",
                    "System.Data.dll",
                    "System.Xml.dll",
                }
            };
            BuildEnvironment buildEnvironment = ((IBuildEngine)this).BuildEnvironment;
            options.ReferencedAssemblies.Add(Path.Combine(buildEnvironment.BinDirectory.FullName, "NBuild.Fx.dll"));
            foreach (var reference in _references)
                options.ReferencedAssemblies.Add(reference);

            var sources = new string[_sources.Count];
            _sources.CopyTo(sources, 0);
            _compilerResults = provider.CompileAssemblyFromSource(options, sources);
        }

        private void Process()
        {
            //Find a public type that has the Build attribute and is derived from NBuild.Fx.BuildBase
            Type[] types = _compilerResults.CompiledAssembly.GetExportedTypes();
            Type buildType = Array.Find(types, type => type.IsDefined(typeof(BuildAttribute), true) && type.IsSubclassOf(typeof(BuildScript)));
            if (buildType == null)
                throw new ProcessingException("Cannot find build type. A build type must have a Build attribute and should derive from the {0} class", typeof(BuildScript).FullName);

            //The build class should have a public constructor that accepts a IBuildEngine parameter
            ConstructorInfo defaultConstructor = buildType.GetConstructor(new[] { typeof(IBuildEngine) });
            if (defaultConstructor == null)
                throw new ProcessingException("Cannot find default parameterless constuctor on the build type");

            _buildType = buildType;
            var buildAttribute = (BuildAttribute)buildType.GetCustomAttributes(typeof(BuildAttribute), true)[0];

            //Get a list of possible target methods. A target method should not accept any parameters and returns void
            MethodInfo[] targetMethods = Array.FindAll(buildType.GetMethods(),
                method => method.ReturnType == typeof(void) && method.GetParameters().Length == 0);
            if (targetMethods.Length == 0)
                throw new ProcessingException("Cannot find any possible target methods in the build class");

            //Iterate through the target methods and cache them in the _targets collection
            _targets = new TargetCollection();
            foreach (var targetMethod in targetMethods)
            {
                object[] targetAttributes = targetMethod.GetCustomAttributes(typeof(TargetAttribute), true);

                string targetName;
                Target target;
                if (targetAttributes.Length == 0 )
                {
                    targetName = targetMethod.Name;
                    target = new Target(targetMethod, null);
                }
                else
                {
                    var targetAttribute = (TargetAttribute)targetAttributes[0];
                    targetName = string.IsNullOrEmpty(targetAttribute.Name) ? targetMethod.Name : targetAttribute.Name;
                    target = new Target(targetMethod, (TargetAttribute)targetAttributes[0]);
                }
                _targets.Add(targetName, target);
            }

            //Iterate through the _targets collection and build the dependant targets list for each target
            foreach (var targetEntry in _targets)
            {
                if (targetEntry.Value.TargetAttribute == null || string.IsNullOrEmpty(targetEntry.Value.TargetAttribute.DependsOn))
                    continue;
                string[] dependantTargetNames = targetEntry.Value.TargetAttribute.DependsOn.Split(';');
                foreach (var dependantTargetName in dependantTargetNames)
                {
                    if (dependantTargetName.Equals(targetEntry.Key))
                        throw new ProcessingException("Target cannot depend on itself");

                    Target matchingTarget;
                    if (!_targets.TryGetValue(dependantTargetName, out matchingTarget))
                        throw new ProcessingException("Dependant target {0} not found", dependantTargetName);
                    targetEntry.Value.DependantTargets.Add(matchingTarget);
                }
            }

            //Figure out the list of targets to run at start-up.
            if (_startupTargets.Count > 0)
            {
                _initialTargets = new string[_startupTargets.Count];
                for (int targetIdx = 0; targetIdx < _startupTargets.Count; targetIdx++)
                {
                    string targetName = _startupTargets[targetIdx];
                    if (!_targets.ContainsKey(targetName))
                        throw new ProcessingException("Cannot find specified target named {0}", targetName);
                    _initialTargets[targetIdx] = targetName;
                }
            }
            else if (!string.IsNullOrEmpty(buildAttribute.DefaultTarget))
            {
                if (!_targets.ContainsKey(buildAttribute.DefaultTarget))
                    throw new ProcessingException("Cannot find default build target named {0}", buildAttribute.DefaultTarget);
                _initialTargets = new[] { buildAttribute.DefaultTarget };
            }
            else
            {
                using (IEnumerator<string> enumerator = _targets.Keys.GetEnumerator())
                    if (enumerator.MoveNext())
                        _initialTargets = new[] { enumerator.Current };
            }

            //Build the property mappings cache of all settable public properties that are not indexers.
            //Also check which of these properties can be assigned from a string, either directly or through a type converter
            PropertyInfo[] properties = Array.FindAll(_buildType.GetProperties(),
                property =>
                {
                    if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length > 0)
                        return false;
                    if (property.PropertyType == typeof(string))
                        return true;
                    TypeConverter converter = TypeDescriptor.GetConverter(property.PropertyType);
                    return converter != null && converter.CanConvertFrom(typeof(string));
                });

            foreach (var property in properties)
                _propertyMappings.Add(property.Name, property);

            //Verify that all property overrides specify valid property names
            foreach (var propertyOverride in _propertyOverrides)
                if (!_propertyMappings.ContainsKey(propertyOverride.Name))
                    throw new ProcessingException("Property {0} not found in the build object", propertyOverride.Name);
        }

        private void Run()
        {
            //Create an instance of the build class
            _buildInstance = (NBuildScript)Activator.CreateInstance(_buildType, new[] { this });
            using (_buildInstance)
            {
                //Assign the property overrides
                foreach (var propertyOverride in _propertyOverrides)
                {
                    PropertyInfo property = _propertyMappings[propertyOverride.Name];
                    if (property.PropertyType == typeof(string))
                        property.SetValue(_buildInstance, propertyOverride.Value, null);
                    else
                    {
                        TypeConverter converter = TypeDescriptor.GetConverter(property.PropertyType);
                        if (!converter.CanConvertFrom(typeof(string)))
                            throw new ProcessingException("Cannot set property {0} of type {1} to string value '{2}'", property.Name, property.PropertyType, propertyOverride.Value);
                        object propertyValue = converter.ConvertFromString(propertyOverride.Value);
                        property.SetValue(_buildInstance, propertyValue, null);
                    }
                }

                //Execute the default target
                var executedTargets = new List<Target>(_targets.Count);
                foreach (var initialTarget in _initialTargets)
                {
                    Target target = _targets[initialTarget];
                    RunTarget(target, executedTargets);
                }
            }
        }

        private void RunTarget(Target target, ICollection<Target> executedTargets)
        {
            if (executedTargets.Contains(target))
                return;

            foreach (var dependantTarget in target.DependantTargets)
                RunTarget(dependantTarget, executedTargets);

            target.Method.Invoke(_buildInstance, null);
            executedTargets.Add(target);
        }

        #endregion

        #region IBuildEngine members

        TTaskSet IBuildEngine.GetTask<TTaskSet>()
        {
            Type taskType = typeof(TTaskSet);
            object taskInstance;
            if (_taskPool.TryGetValue(taskType, out taskInstance))
                return (TTaskSet)taskInstance;
            
            taskInstance = Activator.CreateInstance(taskType, this, _buildInstance);
            if (((ITaskSet)taskInstance).IsReusable)
                _taskPool.Add(taskType, taskInstance);
            return (TTaskSet)taskInstance;
        }

        BuildEnvironment IBuildEngine.BuildEnvironment
        {
            get { return _buildEnvironment; }
        }

        #endregion

        #region IDisposable members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~NBuildEngine()
        {
            Dispose(false);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
                AppDomain.CurrentDomain.AssemblyResolve -= AppDomain_AssemblyResolve;
        }

        #endregion
    }

    public enum ExecutionMode
    {
        Run,
        Verify,
        Debug
    }
}
